﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace KingOfCSharp.iHunter.Plugins.BBS
{
    public class BbsClient : IBbsClient, IDisposable
    {
        private TaskExecutor executor;

        private Queue<Task> taskQueue;

        private Dictionary<Task, ManualResetEvent> taskResetEvents;
        private Dictionary<Task, TaskResult> taskResults;
        private Dictionary<Task, int> retryTimes;

        private bool canceled;

        public int MaxRetry
        {
            get;
            set;
        }

        private Timer nextTaskTimer;
        private Timer enableSendTimer;
        private ManualResetEvent sendBlockResetEvent;
        private int retryInterval;

        private Queue<object> itemDownloaded;
        private bool downloadFinished;
        private Exception downloadException;
        private AutoResetEvent itemDownloadResetEvent;
        private bool retryDownloadTask;

        public BbsClient(BbsHost host)
        {
            this.executor = new TaskExecutor(host);

            this.taskQueue = new Queue<Task>();
            this.taskResetEvents = new Dictionary<Task, ManualResetEvent>();
            this.taskResults = new Dictionary<Task, TaskResult>();
            this.retryTimes = new Dictionary<Task, int>();

            this.MaxRetry = 1;
            this.retryInterval = 18000;

            this.nextTaskTimer = new Timer(new TimerCallback(ExecuteNextTask));
            this.enableSendTimer = new Timer(new TimerCallback(EnableSend));
            this.sendBlockResetEvent = new ManualResetEvent(true);
            this.itemDownloadResetEvent = new AutoResetEvent(true);

            this.downloadFinished = true;
            this.itemDownloaded = new Queue<object>();

            this.executor.TaskCompletedHandler += new TaskExecutor.TaskCompletedEventHandler(OnTaskFinished);
            this.executor.ItemDownloadedEventHandler += this.OnItemDownloaded;

            this.retryDownloadTask = false;
            this.canceled = false;
            
        }

        private object WaitForTaskComplete(Task task)
        {
            if (null == task)
                return null;

            if (canceled)
                return ErrorResult(task);

            if (taskResetEvents.ContainsKey(task))
                return null;

            ManualResetEvent resetEvent = new ManualResetEvent(false);
            Monitor.Enter(taskResetEvents);
            taskResetEvents.Add(task, resetEvent);
            Monitor.Exit(taskResetEvents);
            retryTimes.Add(task, 0);

            Task header = null;
            
            Monitor.Enter(this.taskQueue);
            if (this.taskQueue.Count == 0)
            {
                nextTaskTimer.Change(Timeout.Infinite, Timeout.Infinite);
                header = task;
            }

            taskQueue.Enqueue(task);            
            Monitor.Exit(this.taskQueue);

            if (null != header)
            {
                sendBlockResetEvent.WaitOne();
                executor.ExecuteTask(header);
            }

            resetEvent.WaitOne();

            Monitor.Enter(taskResults);
            object result = taskResults[task].Output;

            taskResults.Remove(task);
            Monitor.Exit(taskResults);

            Monitor.Enter(taskResetEvents);
            (taskResetEvents[task] as IDisposable).Dispose();
            taskResetEvents.Remove(task);
            retryTimes.Remove(task);
            Monitor.Exit(taskResetEvents);
            

            return result;            
        }

        private void OnItemDownloaded(object sender, ItemDownloadedEventArgs args)
        {
            Monitor.Enter(this.itemDownloadResetEvent);
            this.itemDownloaded.Enqueue(args.Item);
            this.itemDownloadResetEvent.Set();

            Monitor.Exit(this.itemDownloadResetEvent);
        }

        // ToDo : when task is finished
        private void OnTaskFinished(object sender, TaskCompletedEventArgs args)
        {
            if (canceled)
                return;

            Task task = args.Task;
            if (task.TaskType == Task.Type.SetMessageOption || task.TaskType == Task.Type.Logout)
                return;

            System.Diagnostics.Debug.Assert(taskResetEvents.ContainsKey(task));

			System.Diagnostics.Debug.WriteLine(string.Format("Task: {0}, Success: {1}, Remark: {2}",
				task.TaskType,
				args.Success,
				args.Error));

            if (args.Success || (!args.Retry) || (retryTimes[task] >= MaxRetry))
            {
                if (canceled)
                    return;

                if (!args.Success)
                {
                    if (null != this.ErrorHandler)
                        this.ErrorHandler(this, new PluginErrorEventArgs(this, args.Error.Message));
                }

                switch (task.TaskType)
                {
                    case Task.Type.CreateThread:
                    case Task.Type.ReplyPost:
                    case Task.Type.ReplyMail:
                    case Task.Type.SendMail:
                        Monitor.Enter(taskResults);
                        taskResults[task] = new TaskResult(task, args.Success, args.Error, args.Result);
                        Monitor.Exit(taskResults);

                        Monitor.Enter(taskResetEvents);
                        taskResetEvents[task].Set();
                        Monitor.Exit(taskResetEvents);

                        break;

                    case Task.Type.DownloadMails:
                    case Task.Type.DownloadPosts:

                        Monitor.Enter(this.itemDownloadResetEvent);
                        //executor.ItemDownloadedEventHandler -= this.OnItemDownloaded;
                        downloadException = args.Error;
                        downloadFinished = true;

                        itemDownloadResetEvent.Set();

                        Monitor.Exit(this.itemDownloadResetEvent);
                        
                        Monitor.Enter(taskResetEvents);
                        (taskResetEvents[task] as IDisposable).Dispose();
                        taskResetEvents.Remove(task);
                        retryTimes.Remove(task);
                        Monitor.Exit(taskResetEvents);

                        break;
                }

                

                //Task queueHead = null;

                Monitor.Enter(this.taskQueue);
                //System.Diagnostics.Debug.Assert(task == taskQueue.Dequeue());
                taskQueue.Dequeue();

                switch (task.TaskType)
                {
                    case Task.Type.CreateThread:
                    case Task.Type.ReplyPost:
                    case Task.Type.ReplyMail:
                    case Task.Type.SendMail:
                        sendBlockResetEvent.Reset();
                        enableSendTimer.Change(2000, Timeout.Infinite);
                        break;
                }

                if (taskQueue.Count > 0)
                {
                    nextTaskTimer.Change(100, Timeout.Infinite);
                }

                /*if (taskQueue.Count > 0)
                {
                    System.Diagnostics.Debug.WriteLine(string.Format("Queue length: {0}", taskQueue.Count));
                    queueHead = taskQueue.Peek();
                }*/

                Monitor.Exit(this.taskQueue);

                /*if (null != queueHead)
                {
                    nextTaskResetEvent.WaitOne();                    
                    executor.ExecuteTask(queueHead);
                }*/
            }
            else
            {
                retryTimes[task]++;
                this.retryDownloadTask = true;

                sendBlockResetEvent.Reset();
                enableSendTimer.Change(2000, Timeout.Infinite);
                if (this.retryInterval <= 300000)
                    retryInterval += 2000;

                nextTaskTimer.Change(retryInterval, Timeout.Infinite);

                //nextTaskResetEvent.WaitOne();
                //executor.ExecuteTask(task);
            }

            //System.Diagnostics.Debug.WriteLine("Task Finished");
        }

        private void EnableSend(object arg)
        {
            this.sendBlockResetEvent.Set();
        }

        private void ExecuteNextTask(object arg)
        {
            sendBlockResetEvent.WaitOne();

            Task task = null;
            Monitor.Enter(this.taskQueue);

            System.Diagnostics.Debug.Assert(taskQueue.Count > 0);
            if (this.taskQueue.Count > 0)
            {
                task = taskQueue.Peek();                
            }

            Monitor.Exit(this.taskQueue);

            if (null != task)
            {
                switch (task.TaskType)
                {
                    case Task.Type.CreateThread:
                    case Task.Type.ReplyMail:
                    case Task.Type.SendMail:
                    case Task.Type.ReplyPost:
                        this.executor.ExecuteTask(task);
                        break;

                    case Task.Type.DownloadMails:
                    case Task.Type.DownloadPosts:
                        if (this.retryDownloadTask)
                        {
                            retryDownloadTask = false;

                            this.itemDownloadResetEvent.Reset();
                            this.itemDownloaded = new Queue<object>();
                            this.downloadFinished = false;

                            executor.ExecuteTask(task);
                        }
                        else
                        {
                            Monitor.Enter(taskResetEvents);
                            taskResetEvents[task].Set();
                            Monitor.Exit(taskResetEvents);
                        }

                        break;
                }
                
               
           
            }

            
        }

        public void CancelAllTasks()
        {
            canceled = true;

            Monitor.Enter(taskResults);
            foreach (Task task in this.taskResults.Keys)
            {
                taskResults[task] = new TaskResult(task, false, new InvalidOperationException("Manually cancelled"), ErrorResult(task));
            }
            Monitor.Exit(taskResults);

            Monitor.Enter(taskResetEvents);
            foreach (Task task in this.taskResetEvents.Keys)
            {
                taskResetEvents[task].Set();
            }
            Monitor.Exit(taskResetEvents);
        }

        private object ErrorResult(Task task)
        {
            switch (task.TaskType)
            {
                case Task.Type.DownloadMails:
                    return new List<Mail>();
                    
                case Task.Type.DownloadPosts:
                    return new List<Post>();
                    
                default:
                    return false;
            }
        }

        public PluginErrorEventHandler ErrorHandler;

        #region IBbsClient Members

        public IEnumerable<Mail> DownloadMails(DownloadMailsTask task)
        {
            if (null == task)
                yield break;

            if (canceled)
                yield break;

            if (taskResetEvents.ContainsKey(task))
                yield break;

            ManualResetEvent resetEvent = new ManualResetEvent(false);
            Monitor.Enter(taskResetEvents);
            taskResetEvents.Add(task, resetEvent);
            Monitor.Exit(taskResetEvents);
            retryTimes.Add(task, 0);

            Task header = null;

            Monitor.Enter(this.taskQueue);
            if (this.taskQueue.Count == 0)
            {
                nextTaskTimer.Change(Timeout.Infinite, Timeout.Infinite);
                header = task;
            }

            taskQueue.Enqueue(task);
            Monitor.Exit(this.taskQueue);

            if (null != header)
            {
//                sendBlockResetEvent.WaitOne();
                resetEvent.Set();                
                //executor.ItemDownloadedEventHandler += this.OnItemDownloaded;
            }

            resetEvent.WaitOne();
        

            System.Diagnostics.Debug.Assert(task == this.taskQueue.Peek());

            this.itemDownloadResetEvent.Reset();
            this.itemDownloaded = new Queue<object>();
            this.downloadFinished = false;               

            executor.ExecuteTask(task);

            while (true)
            {
                this.itemDownloadResetEvent.WaitOne();
                Monitor.Enter(this.itemDownloadResetEvent);
                if (downloadFinished)
                {
                    while (itemDownloaded.Count > 0)
                    {
                        object item = itemDownloaded.Dequeue();

                        System.Diagnostics.Debug.Assert(item is Mail);

                        yield return item as Mail;
                    }

                    Monitor.Exit(this.itemDownloadResetEvent);

                    if (null == downloadException)
                        yield break;
                    else
                    {
                        if (downloadException is System.Net.WebException ||
                            downloadException is System.Net.Sockets.SocketException ||
                            downloadException is InvalidOperationException)
                            throw downloadException;
                        else
                        {
                            throw new System.Net.WebException(downloadException.Message);
                        }
                    }
                }
                else
                {
                    System.Diagnostics.Debug.Assert(itemDownloaded.Count > 0);
                    while (itemDownloaded.Count > 0)
                    {
                        object item = itemDownloaded.Dequeue();

                        System.Diagnostics.Debug.Assert(item is Mail);

                        yield return item as Mail;
                    }
                }
                Monitor.Exit(this.itemDownloadResetEvent);
            }

//             resetEvent.WaitOne();

            // when task finishes
            /*Monitor.Enter(taskResults);
            object result = taskResults[task].Output;

            taskResults.Remove(task);
            Monitor.Exit(taskResults);

            Monitor.Enter(taskResetEvents);
            (taskResetEvents[task] as IDisposable).Dispose();
            taskResetEvents.Remove(task);
            retryTimes.Remove(task);
            Monitor.Exit(taskResetEvents);


            return result;        */

            
        }

        public bool SendMail(SendMailTask task)
        {
            object result = WaitForTaskComplete(task);

            return (bool)result;
        }

        public bool ReplyMail(ReplyMailTask task)
        {
            object result = WaitForTaskComplete(task);

            return (bool)result;
        }

        public bool CreateThread(CreateThreadTask task)
        {
            object result = WaitForTaskComplete(task);

            return (bool)result;
        }

        public bool ReplyPost(ReplyPostTask task)
        {
            object result = WaitForTaskComplete(task);

            return (bool)result;
        }

        public IEnumerable<Post> DownloadPosts(DownloadPostsTask task)
        {
            if (null == task)
                yield break;

            if (canceled)
                yield break;

            if (taskResetEvents.ContainsKey(task))
                yield break;

            ManualResetEvent resetEvent = new ManualResetEvent(false);
            Monitor.Enter(taskResetEvents);
            taskResetEvents.Add(task, resetEvent);
            Monitor.Exit(taskResetEvents);
            retryTimes.Add(task, 0);

            Task header = null;

            Monitor.Enter(this.taskQueue);
            if (this.taskQueue.Count == 0)
            {
                nextTaskTimer.Change(Timeout.Infinite, Timeout.Infinite);
                header = task;
            }

            taskQueue.Enqueue(task);
            Monitor.Exit(this.taskQueue);

           

            if (null != header)
            {
                //                sendBlockResetEvent.WaitOne();
                resetEvent.Set();
                //executor.ItemDownloadedEventHandler += this.OnItemDownloaded;
            }

            resetEvent.WaitOne();

           
            System.Diagnostics.Debug.Assert(task == this.taskQueue.Peek());

            this.itemDownloadResetEvent.Reset();
            this.itemDownloaded = new Queue<object>();
            this.downloadFinished = false;

            executor.ExecuteTask(task);

            while (true)
            {
                this.itemDownloadResetEvent.WaitOne();
                Monitor.Enter(this.itemDownloadResetEvent);
                if (downloadFinished)
                {
                    while (itemDownloaded.Count > 0)
                    {
                        object item = itemDownloaded.Dequeue();

                        System.Diagnostics.Debug.Assert(item is Post);

                        yield return item as Post;
                    }

                    Monitor.Exit(this.itemDownloadResetEvent);

                    if (null == downloadException)
                        yield break;
                    else
                    {
                        if (downloadException is System.Net.WebException ||
                            downloadException is System.Net.Sockets.SocketException ||
                            downloadException is InvalidOperationException)
                        throw downloadException;
                        else
                        {
                            throw new System.Net.WebException(downloadException.Message);
                        }
                    }
                }
                else
                {
                    //System.Diagnostics.Debug.Assert(itemDownloaded.Count > 0);
                    while (itemDownloaded.Count > 0)
                    {
                        object item = itemDownloaded.Dequeue();

                        System.Diagnostics.Debug.Assert(item is Post);

                        yield return item as Post;
                    }
                }
                Monitor.Exit(this.itemDownloadResetEvent);
            }
        }

        #endregion


        #region IDisposable Members

        public void Dispose()
        {
            canceled = true;
            executor.Disconnect();

            this.taskQueue.Clear();
            this.taskQueue = null;
            this.taskResetEvents.Clear();
            this.taskResetEvents = null;
            this.taskResults.Clear();
            this.taskResults = null;
            this.retryTimes.Clear();
            this.retryTimes = null;

            nextTaskTimer.Dispose();
            enableSendTimer.Dispose();
            (sendBlockResetEvent as IDisposable).Dispose();
            (itemDownloadResetEvent as IDisposable).Dispose();
        }

        #endregion
    }
}
